Cache-busting bundles with assetUrl

Best practice By design
Browsers cache static files, so an unchanged bundle URL can serve stale JS/CSS for up to a day after a publish. Wrap your own <script src> and <link href> in assetUrl() to append a per-publish ?v= stamp that busts the cache. Never use assetUrl on dynamic or per-user output — ?v= URLs get max-age=1 year immutable, which would freeze the first dynamic response for everyone.

What you'll see

A developer publishes an updated JS or CSS bundle, but visitors keep running the old version — sometimes for up to a day — because the bundle URL never changed and the browser saw no reason to refetch it. The symptom is not just wrong styling: an old JS bundle running against new HTML causes functional breakage, and it clears itself only when the browser cache expires or the user hard-refreshes.

What's actually happening

Browsers cache static assets aggressively. If the URL of a bundle stays the same across a publish, the browser treats the cached copy as current and does not refetch it. docly.assetUrl(path) solves this by appending ?v=<version> to the path, where the version is the site's last-modified timestamp formatted yyyyMMddHHmmss. The stamp is site-wide (not per file) and changes on every publish.

The server backs this up: any response whose URL carries a ?v= query is served with Cache-Control: max-age=31536000, immutable (one year). So an unchanged bundle is never refetched unnecessarily, and a changed bundle gets a brand-new URL after a publish and is refetched immediately. Public HTML without ?v= is served no-cache instead, so it revalidates cheaply (a 304 via Last-Modified). External URLs (containing :// or starting with //) are returned unchanged, with no ?v=.

What to do

Wrap all of your own <script src> and <link href> references in docly.assetUrl(...):

<script src="#docly.assetUrl('/assets/js/bundle.js')#"></script>
<link href="#docly.assetUrl('/assets/css/site.css')#" rel="stylesheet">

The bundle now gets ?v=<version>. A new publish means a new version, a new URL, and an immediate refetch by the browser. The server sets immutable, max-age=1 year on ?v= URLs, so unchanged bundles are never refetched unnecessarily — this also scores full marks on Google's "efficient cache policy" audit.

⚠ Does NOT work for your own dynamic output. assetUrl versions by publish time (the site's last-modified timestamp), not by the actual content of a dynamically generated response. So do not use assetUrl on a URL that produces content which varies without a publish:

  • output that depends on query parameters, the logged-in user, the current time, counters, or external sources
  • anything that must be fresh per request

Why it is dangerous: a ?v= URL gets immutable, max-age=1 year. If the browser or CDN caches the first dynamic response, that same response is served to everyone for a whole year — stale content, and potentially one user's data leaking to others.

What to do instead: serve dynamic or per-user content from a .js API endpoint under #/API/. API responses are no-cache by default and must not be versioned with assetUrl. Fetch them from the browser at runtime.

Rule of thumb:

  • Static bundle/asset (changes only on publish) → use assetUrl.
  • Dynamic / per-user / per-request output → do not — serve it as a .js API (no-cache).
  • External URL (CDN etc.) → no — assetUrl leaves it unchanged anyway.